from string import Formatter
#import textwrap
#NB: Now defaults to 's' conversions - suspect this might be slow

import inspect
from collections import Mapping, Sequence

def cleandoc(text):
    """ like inspect.cleandoc, but doesn't strip trailing \n """
    lastchar = '\n' if text.endswith('\n') else ''
    return inspect.cleandoc(text) + lastchar

class NestedFormatter(Formatter):
    """ Provide formatting of data using nested templates """
    
    def __init__(self, templates, dedent=True):
        """ templates: a dictionary {'template_key':'template', ...}
                       where the template strings are as used by str.format(), i.e. have '{replacement_field'}
                       but fields may also be other template_keys.
            
            dedent: set True if you want to include multi-line templates using triple-quoted strings like:
                        'multi_line':'''this is a multi-line template
                                        but leading whitespace will be
                                        stripped from the 2nd line onwards'''
                    set False to preserve white-space as written
        """
        
        self.templates = templates
        if dedent:
            for k, v in self.templates.items():
                if '\n' in v:   # only want to clean up multi-line templates
                    #print 'have multiline template', repr(k), ':\n', repr(v)
                    cleantemplate = cleandoc(self.templates[k])
                    #print 'cleaned:\n', repr(cleantemplate)
                    self.templates[k] = cleantemplate

    def format(self, template_name, valdict):
        """ return the specified template with fields recursively replaced using valdict """
        
        try:
            template_str = self.templates[template_name]
        except KeyError:
            raise KeyError('no template "%s" defined' % template_name)
            
        text = ''
        for (literal_text, field_name, modifier, conversion) in self.parse(template_str):
            text += literal_text
            
            if field_name is None: # have literal only
                continue
                
            if field_name in valdict:
                val = valdict[field_name]
                if isinstance(val, Sequence) and len(val) and isinstance(val[0], Mapping): # sequence field
                    seq = []
                    for item in val:
                        if 'template' not in item:
                            raise KeyError("sequence values must contain a 'template' key:value pair (failed in field '%s')" % field_name)
                        template = '%s.%s' % (field_name, item['template'])
                        if modifier:
                            template += ':' + modifier
                            
                        seq.append(self.format(template, item))
                    
                    # join the lot using the provided separator, or the empty string if none defined
                    sep = self.templates.get("%s.sep" % field_name, '')
                    text += sep.join(seq)
                    
                else: # single field
                    text += str(val)
            
            elif field_name in self.templates: # prefer a field value to a template if both are available
                text += self.format(field_name, valdict)
                
            else:
                raise KeyError('in template "%s" there is no template or value for field "%s"' % (template_name, field_name))            
        return text

if __name__ == '__main__':
    
    it1 = {'a':'a', 'b':'b', 'c':'c', 'template':'it1'}
    it2 = {'d':'d', 'e':'e', 'template':'it2'}
    vals = {'seq':(it2, it1)}
    
    templates = {'test':'no modifier: {seq}\nmodifier: {seq:b}',
                 'it1':'{template} {a} {b} {c} ',
                 'it2':'{template} {d} {e} ',
                 'it1b':'{template} {a} - {b} - {c} ',
                 'it2b':'{template} {d} - {e} ',
                }
    fmter = NestedFormatter(templates)
    res = fmter.format('test', vals)
    print 'result:'
    print res